package com.jwgf.fram;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;

import com.jwgf.common.AppContext;
import com.jwgf.common.Utility;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class FramManager {
    private static final String KEY_VERSION = "This is fram data version key.";
    private static final String KEY_USED_SIZE = "This is fram used size key.";
    private static final int BYTE_TYPE = 1;
    private static final int SHORT_TYPE = 2;
    private static final int INT_TYPE = 3;
    private static final int LONG_TYPE = 4;
    private static final int FLOAT_TYPE = 5;
    private static final int DOUBLE_TYPE = 6;
    private static final int FRAM_TOTAL_SIZE = 32 * 1024;
    private static boolean isUpdatedFrm = false;
    private byte[] fram = new byte[FRAM_TOTAL_SIZE];
//    private HashSet<String> framErrorSet;

    private static FramManager framManager = new FramManager();
    private static Context ctx = AppContext.getInstance();
    private static SharedPreferences sp = ctx.getSharedPreferences("fram",
            Context.MODE_PRIVATE);
    //private static HashMap<String,SharedPreferences> framError=new HashMap<String,SharedPreferences>();
    private static SharedPreferences.Editor editor = sp.edit();
    private static int usedSize = sp.getInt(KEY_USED_SIZE, 0);
    private List<OnFramChangedListener> listeners = new ArrayList<FramManager.OnFramChangedListener>();

    private FramManager() {
        super();
        fram = FramInterface.getInstance().read(0, FRAM_TOTAL_SIZE);
    }

    public static FramManager getInstance() {
        return framManager;
    }


    public static FramManager create(String ver) {

        if (!sp.contains(KEY_VERSION)
                || !sp.getString(KEY_VERSION, null).equals(ver)) {
            upgrade(ver);
        }
        return framManager;
    }

    private static void upgrade(String ver) {
        editor.clear();
        editor.putString(KEY_VERSION, ver);
        editor.putInt(KEY_USED_SIZE, 0);

        editor.commit();
        usedSize = 0;
    }

    public Set<String> keySet() {
        return sp.getAll().keySet();
    }

    public FramManager putByte(String key, byte value) throws IOException {
        byte[] data = new byte[1];
        data[0] = value;
        if (value != getByte(key, 0)) {

            if (sp.contains(key)) {
                int addr_and_type = sp.getInt(key, 0);
                int addr = (addr_and_type & 0xffff0000) >>> 16;
                int type = addr_and_type & 0x0000ffff;
                if (type != BYTE_TYPE) {
                    throw new ClassCastException(buildExceptionString(key, type)
                            + "byte");
                }
                System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                FramInterface.getInstance().write(addr, data);

            } else {
                int addr = usedSize;
                int type = BYTE_TYPE;
                if (usedSize + getTypeByteSize(type) < FRAM_TOTAL_SIZE) {
                    int addr_and_type = (addr << 16) | type;
                    editor.putInt(key, addr_and_type);
                    usedSize += getTypeByteSize(type);
                    editor.putInt(KEY_USED_SIZE, usedSize);
                    editor.commit();
                    System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                    FramInterface.getInstance().write(addr, data);
                } else {
                    throw new IOException("Fram is full!");
                }
            }

            notifyAllListeners(key);
        }
        return framManager;
    }

    public byte getByte(String key, int defValue) {
        if (sp.contains(key)) {
            int addr_and_type = sp.getInt(key, 0);
            int addr = (addr_and_type & 0xffff0000) >>> 16;
            int type = addr_and_type & 0x0000ffff;
            if (type != BYTE_TYPE) {
                throw new ClassCastException(buildExceptionString(key, type)
                        + "byte");
            }
            byte[] data = new byte[getTypeByteSize(type)];
            System.arraycopy(fram, addr, data, 0, data.length);
            return data[0];
        }
        return (byte) defValue;
    }

    public FramManager putShort(String key, short value) throws IOException {
        byte[] data = Utility.ByteArrayUtils.short2ByteArray(value);

        if (value != getShort(key, 0)) {

            if (sp.contains(key)) {
                int addr_and_type = sp.getInt(key, 0);
                int addr = (addr_and_type & 0xffff0000) >>> 16;
                int type = addr_and_type & 0x0000ffff;
                if (type != SHORT_TYPE) {
                    throw new ClassCastException(buildExceptionString(key, type)
                            + "short");
                }
                System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                FramInterface.getInstance().write(addr, data);

            } else {
                int addr = usedSize;
                int type = SHORT_TYPE;
                if (usedSize + getTypeByteSize(type) < FRAM_TOTAL_SIZE) {
                    int addr_and_type = (addr << 16) | type;
                    editor.putInt(key, addr_and_type);
                    usedSize += getTypeByteSize(type);
                    editor.putInt(KEY_USED_SIZE, usedSize);
                    editor.commit();
                    System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                    FramInterface.getInstance().write(addr, data);
                } else {
                    throw new IOException("Fram is full!");
                }
            }
            notifyAllListeners(key);
        }
        return framManager;
    }

    public short getShort(String key, int defValue) {
        if (sp.contains(key)) {
            int addr_and_type = sp.getInt(key, 0);
            int addr = (addr_and_type & 0xffff0000) >>> 16;
            int type = addr_and_type & 0x0000ffff;
            if (type != SHORT_TYPE) {
                throw new ClassCastException(buildExceptionString(key, type)
                        + "short");
            }
            byte[] data = new byte[getTypeByteSize(type)];
            System.arraycopy(fram, addr, data, 0, data.length);
            return Utility.ByteArrayUtils.byteArray2Short(data);
        }
        return (short) defValue;
    }

    public FramManager putInt(String key, int value) throws IOException {
        byte[] data = Utility.ByteArrayUtils.int2ByteArray(value);
        if (value != getInt(key, 0)) {
            if (sp.contains(key)) {
                int addr_and_type = sp.getInt(key, 0);
                int addr = (addr_and_type & 0xffff0000) >>> 16;
                int type = addr_and_type & 0x0000ffff;
                if (type != INT_TYPE) {
                    throw new ClassCastException(buildExceptionString(key, type)
                            + "int");
                }
                System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                FramInterface.getInstance().write(addr, data);

            } else {
                int addr = usedSize;
                int type = INT_TYPE;
                if (usedSize + getTypeByteSize(type) < FRAM_TOTAL_SIZE) {
                    int addr_and_type = (addr << 16) | type;
                    editor.putInt(key, addr_and_type);
                    usedSize += getTypeByteSize(type);
                    editor.putInt(KEY_USED_SIZE, usedSize);
                    editor.commit();
                    System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                    FramInterface.getInstance().write(addr, data);
                } else {
                    throw new IOException("Fram is full!");
                }
            }
            notifyAllListeners(key);
        }
        return framManager;
    }

    public int getInt(String key, int defValue) {
        if (sp.contains(key)) {
            int addr_and_type = sp.getInt(key, 0);
            int addr = (addr_and_type & 0xffff0000) >>> 16;
            int type = addr_and_type & 0x0000ffff;
            if (type != INT_TYPE) {
                throw new ClassCastException(buildExceptionString(key, type) + "int");
            }
            byte[] data = new byte[getTypeByteSize(type)];
            System.arraycopy(fram, addr, data, 0, data.length);
            return Utility.ByteArrayUtils.byteArray2Int(data);
        }
        return defValue;
    }

    public FramManager putLong(String key, long value) throws IOException {
        byte[] data = Utility.ByteArrayUtils.long2ByteArray(value);
        if (value != getLong(key, 0)) {
            if (sp.contains(key)) {
                int addr_and_type = sp.getInt(key, 0);
                int addr = (addr_and_type & 0xffff0000) >>> 16;
                int type = addr_and_type & 0x0000ffff;
                if (type != LONG_TYPE) {
                    throw new ClassCastException(buildExceptionString(key, type)
                            + "long");
                }
                System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                FramInterface.getInstance().write(addr, data);

            } else {
                int addr = usedSize;
                int type = LONG_TYPE;
                if (usedSize + getTypeByteSize(type) < FRAM_TOTAL_SIZE) {
                    int addr_and_type = (addr << 16) | type;
                    editor.putInt(key, addr_and_type);
                    usedSize += getTypeByteSize(type);
                    editor.putInt(KEY_USED_SIZE, usedSize);
                    editor.commit();
                    System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                    FramInterface.getInstance().write(addr, data);
                } else {
                    throw new IOException("Fram is full!");
                }
            }
            notifyAllListeners(key);
        }
        return framManager;
    }

    public long getLong(String key, long defValue) {
        if (sp.contains(key)) {
            int addr_and_type = sp.getInt(key, 0);
            int addr = (addr_and_type & 0xffff0000) >>> 16;
            int type = addr_and_type & 0x0000ffff;
            if (type != LONG_TYPE) {
                throw new ClassCastException(buildExceptionString(key, type)
                        + "long");
            }
            byte[] data = new byte[getTypeByteSize(type)];
            System.arraycopy(fram, addr, data, 0, data.length);
            return Utility.ByteArrayUtils.byteArray2Long(data);
        }
        return defValue;
    }

    public FramManager putFloat(String key, float value) throws IOException {
        byte[] data = Utility.ByteArrayUtils.float2ByteArray(value);
        if (!String.valueOf(value).equals(String.valueOf(getFloat(key, 0)))) {
            if (sp.contains(key)) {
                int addr_and_type = sp.getInt(key, 0);
                int addr = (addr_and_type & 0xffff0000) >>> 16;
                int type = addr_and_type & 0x0000ffff;
                if (type != FLOAT_TYPE) {
                    throw new ClassCastException(buildExceptionString(key, type)
                            + "float");
                }
                System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                FramInterface.getInstance().write(addr, data);

            } else {
                int addr = usedSize;
                int type = FLOAT_TYPE;
                if (usedSize + getTypeByteSize(type) < FRAM_TOTAL_SIZE) {
                    int addr_and_type = (addr << 16) | type;
                    editor.putInt(key, addr_and_type);
                    usedSize += getTypeByteSize(type);
                    editor.putInt(KEY_USED_SIZE, usedSize);
                    editor.commit();
                    System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                    FramInterface.getInstance().write(addr, data);
                } else {
                    throw new IOException("Fram is full!");
                }
            }
            notifyAllListeners(key);
        }
        return framManager;
    }

    public float getFloat(String key, float defValue) {
        if (sp.contains(key)) {
            int addr_and_type = sp.getInt(key, 0);
            int addr = (addr_and_type & 0xffff0000) >>> 16;
            int type = addr_and_type & 0x0000ffff;
            if (type != FLOAT_TYPE) {
                throw new ClassCastException(buildExceptionString(key, type)
                        + "float");
            }
            byte[] data = new byte[getTypeByteSize(type)];
            System.arraycopy(fram, addr, data, 0, data.length);
            return Utility.ByteArrayUtils.byteArray2Float(data);
        }
        return defValue;
    }

    public FramManager putDouble(String key, double value) throws IOException {
        byte[] data = Utility.ByteArrayUtils.double2ByteArray(value);
        if (!String.valueOf(value).equals(String.valueOf(getDouble(key, 0)))) {
            if (sp.contains(key)) {
                int addr_and_type = sp.getInt(key, 0);
                int addr = (addr_and_type & 0xffff0000) >>> 16;
                int type = addr_and_type & 0x0000ffff;
                if (type != DOUBLE_TYPE) {
                    throw new ClassCastException(buildExceptionString(key, type)
                            + "double");
                }
                System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                FramInterface.getInstance().write(addr, data);

            } else {
                int addr = usedSize;
                int type = DOUBLE_TYPE;
                if (usedSize + getTypeByteSize(type) < FRAM_TOTAL_SIZE) {
                    int addr_and_type = (addr << 16) | type;
                    editor.putInt(key, addr_and_type);
                    usedSize += getTypeByteSize(type);
                    editor.putInt(KEY_USED_SIZE, usedSize);
                    editor.commit();
                    System.arraycopy(data, 0, fram, addr, getTypeByteSize(type));
                    FramInterface.getInstance().write(addr, data);
                } else {
                    throw new IOException("Fram is full!");
                }
            }
            notifyAllListeners(key);
        }
        return framManager;
    }

    public double getDouble(String key, double defValue) {
        if (sp.contains(key)) {
            int addr_and_type = sp.getInt(key, usedSize);
            int addr = (addr_and_type & 0xffff0000) >>> 16;
            int type = addr_and_type & 0x0000ffff;
            if (type != DOUBLE_TYPE) {
                throw new ClassCastException(buildExceptionString(key, type)
                        + "double");
            }
            byte[] data = new byte[getTypeByteSize(type)];
            System.arraycopy(fram, addr, data, 0, data.length);
            return Utility.ByteArrayUtils.byteArray2Double(data);
        }
        return defValue;
    }

    public int getUsedSize() {
        return usedSize;
    }

    private int getTypeByteSize(int type) {
        int size = 0;
        if (type == BYTE_TYPE) {
            size = 1;
        } else if (type == SHORT_TYPE) {
            size = 2;
        } else if (type == INT_TYPE) {
            size = 4;
        } else if (type == FLOAT_TYPE) {
            size = 4;
        } else if (type == LONG_TYPE) {
            size = 8;
        } else if (type == DOUBLE_TYPE) {
            size = 8;
        }
        return size;
    }

    private String buildExceptionString(String key, int type) {
        String str = null;
        if (type == BYTE_TYPE) {
            str = "Can not cast type byte to type ";
        } else if (type == SHORT_TYPE) {
            str = "Can not cast type short to type ";
        } else if (type == INT_TYPE) {
            str = "Can not cast type int to type ";
        } else if (type == FLOAT_TYPE) {
            str = "Can not cast type float to type ";
        } else if (type == LONG_TYPE) {
            str = "Can not cast type long to type ";
        } else if (type == DOUBLE_TYPE) {
            str = "Can not cast type double to type ";
        }
        return "key:" + key + " " + str;
    }

    public interface OnFramChangedListener {
        void onFramChanged(FramManager framManager, String key);
    }

    public void registerOnFramChangedListener(OnFramChangedListener listener) {
        listeners.add(listener);
    }

    public void unregisterOnFramChangedListener(OnFramChangedListener listener) {
        listeners.remove(listener);
    }

    void notifyAllListeners(final String key) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Iterator<OnFramChangedListener> itor = listeners.iterator();
            while (itor.hasNext()) {
                OnFramChangedListener listener = itor
                        .next();
                listener.onFramChanged(framManager, key);
            }
        } else {
            // Run this function on the main thread.
            Handler mainHandler = new Handler(ctx.getMainLooper());
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    notifyAllListeners(key);
                }
            });
        }
    }
}
